Skip to content

Go defer、select、timer 与 netpoller:调用结束了,等待怎样结束 ​

defer 管清理,select 等多种条件,timer 把时间变成事件,netpoller 把内核 I/O 就绪变成 goroutine 的执行机会。这四件事经常出现在同一个请求里:建立连接、等待数据或超时、离开函数时清理资源。它们看起来都很简短,但“什么时候求值”“谁被停住”“返回时是否已经停止”决定了程序是否正确。

本篇先把可依赖的语言与 API 语义说清,再看 Go 1.27.2、Linux/amd64 的实现。普通程序、测试、三个 defer 基准以及 timer 兼容反例都实际运行了。netpoller 的 Linux 路径有本地 TCP 实验,kqueue 部分只核对源码,没有在 macOS 上执行。[1][2][3][4]

先分清各自的责任 ​

机制解决什么问题什么时候完成不替你保证什么
defer把清理动作绑定到函数退出当前函数返回、相应 panic 展开等时机不是块结束就执行;进程直接退出时不保证执行
select在多个通信操作中选一个能执行的某个通信就绪,或走 defaultcase 顺序不表示优先级
timer让经过一段时间成为可等待事件到期后产生通知或启动回调到期不表示接收方已经获得 CPU
netpoller让网络等待不用占住每个调用的 OS 线程就绪、关闭或 deadline 等事件唤醒等待者就绪不表示应用协议消息已经完整

理解这些边界,比记运行时内部结构名更有用。下面每一节都从一个能看到的行为开始。[1:1][2:1][3:1][4:1]

defer:参数现在保存,调用以后执行 ​

go
func deferred() (n int) {
    n = 1
    defer fmt.Println("saved argument:", n)
    defer func() { n++; fmt.Println("closure sees:", n) }()
    return 10
}

执行 fmt.Println("return:", deferred()),本次输出:

text
closure sees: 11
saved argument: 1
return: 11

这三行把三条规则接起来了。第一条 defer 执行到时,就保存了 fmt.Println 的参数 n,因此以后打印 1。第二条保存的是一个闭包,它在真正执行时访问命名返回值 n。return 10 先把 n 设为 10,再按后进先出顺序执行 defer;闭包把 n 改为 11,最后返回的也是 11。[1:2]

容易写错的地方原因更稳妥的处理
在很长的循环里反复 defer 关闭资源defer 等的是函数退出,不是本轮循环结束把一次资源使用放进独立函数,或在每轮明确关闭
希望 defer 读到变量最后的值,却直接当参数传入参数在注册时已经求值需要晚读时使用明确的闭包,同时留意捕获对象
认为 os.Exit 会执行清理它直接终止进程,不跑 defer将退出决策留到资源已清理的上层
把 defer 当作正常业务错误的处理机制defer 负责执行时机,不自动处理返回的 error清理错误仍要按调用契约处理

这些是语义与设计建议,不取决于当前编译器怎么优化 defer。[1:3][5]

同样是 defer,实现成本可以差很多 ​

Go 当前主要有两类处理方式:编译器能确定退出路径时,可生成 open-coded defer,把必要信息放在栈槽中,在函数出口直接安排调用;其他情况使用 defer 记录和运行时链。记录本身又可能在栈上,或经堆上的运行时机制管理。panic 展开时,两类路径仍要维护同样的执行语义。[6][7]

正在绘制图示…

可切换适合窗口或原始尺寸
查看图源代码
flowchart TD
    D["执行到 defer<br/>求值并保存调用信息"] --> Q{"编译器能采用<br/>open-coded 路径吗?"}
    Q -->|能| S["栈槽与状态位记录<br/>出口安排清理调用"]
    Q -->|不能| R["建立 defer 记录<br/>接入运行时管理"]
    S --> E["函数退出或 panic 展开<br/>按逆序执行"]
    R --> E
放大图示

原始尺寸下可横向或纵向滚动;按 Escape 关闭。

正在绘制图示…

查看图源代码
flowchart TD
    D["执行到 defer<br/>求值并保存调用信息"] --> Q{"编译器能采用<br/>open-coded 路径吗?"}
    Q -->|能| S["栈槽与状态位记录<br/>出口安排清理调用"]
    Q -->|不能| R["建立 defer 记录<br/>接入运行时管理"]
    S --> E["函数退出或 panic 展开<br/>按逆序执行"]
    R --> E

不能仅靠“有 defer”三个字给一次调用标价格。文末对照里,一个函数直接返回 n+1,一个注册一次 defer 增加命名返回值,还有一个在循环里注册 defer。go build -gcflags='-d=defer' 的实际诊断包括:

text
./main.go:23:32: open-coded defer
./main.go:28:3: heap-allocated defer

同一机器、普通优化构建,go test -run '^$' -bench . -benchmem -count=3 得到:

被测路径三次 ns/op 范围B/opallocs/op
直接返回1.469–1.54100
一次 open-coded defer3.836–4.02800
循环路径,实际只迭代一次41.01–43.34242

这个表说明当前三种完整函数路径有不同成本,不是在给所有 defer 标一个统一单价。循环路径还包含可变返回值的捕获、逃逸和记录管理;记录也可能复用,所以不能把 heap-allocated defer 翻译成“每次固定为一条记录调用一次 malloc”。三次只是小基准复跑,没有跨机器统计或生产负载验证。为了固定调用边界,三个被测函数都用了 //go:noinline。[6:1][7:1]

正常资源清理里,先用 defer 让控制流可靠;只有它确实出现在热点,再比较实现和基准。一个潜在的纳秒级差别,不能替代正确关闭资源。

select:先求值,再选择,不按书写顺序排队 ​

进入 select 时,各通信 case 的 channel 表达式,以及发送右边的值表达式,会按规范求值一次。之后才决定哪个通信可以执行。下面的发送 channel 是 nil,发送不可能成功,但右侧函数仍然执行:[2:2]

go
var disabled chan int
select {
case disabled <- rhs():
default:
    fmt.Println("default selected")
}

rhs 打印一行后返回 1,本次输出:

text
send expression evaluated
default selected

如果右侧是一次昂贵计算或带副作用的调用,即使最后选择别的 case,成本也已经付了。要让计算只在某个业务条件成立后发生,需要重新安排控制流,而不是以为 select 会懒求值。接收赋值左侧的表达式则在选定相应接收后才求值,不能把两者混起来。[2:3]

select 里的条件规范行为使用时的含义
多个通信都可以执行从可执行通信中作均匀的伪随机选择上面的 case 不比下面优先;没有固定轮转顺序
没有通信可执行,有 default执行 default循环里空转 default 可能持续耗 CPU
没有通信可执行,也没 default阻塞要有未来能使某个 case 就绪的路径
某个 channel 是 nil该通信不能执行可临时禁用该分支
已关闭且已读空的 channel 做接收立即得到零值与 false不处理就可能反复选中同一分支

伪随机选择不是业务公平性的硬时限,也不能用来表达“取消一定压过结果”。若需要优先级,应明确设计协议和状态检查,不靠排列 case。[2:4]

关闭后退出或禁用分支尤其重要。本次程序在第一次发现关闭后将变量设为 nil:

go
c := make(chan int)
close(c)
for i := range 2 {
    select {
    case _, ok := <-c:
        fmt.Println("closed ready:", i, ok)
        c = nil
    default:
        fmt.Println("nil disabled:", i)
    }
}
text
closed ready: 0 false
nil disabled: 1

第二轮不再从关闭通道接收。这只是展示禁用技巧;真实循环如果所有分支最终都设成 nil,又没有退出条件,也可能永远等待。[2:5]

运行时怎样同时等待多个 channel ​

当前一般路径 selectgo 会构造随机化的检查顺序,并按一致顺序锁住涉及的 channel,避免多个 select 因锁顺序相反互相卡住。若没有 case 就绪且必须阻塞,就把等待记录挂到相关 channel 上,停放 G;被其中一个通信唤醒后,再清理其他没被选中的等待记录。[8]

正在绘制图示…

可切换适合窗口或原始尺寸
查看图源代码
flowchart TD
    Eval["求值 channel 与发送值"] --> Poll["检查各通信是否就绪<br/>一般路径使用随机化检查顺序"]
    Poll --> Ready{"存在可执行通信?"}
    Ready -->|是| Execute["完成一个通信<br/>执行对应 case"]
    Ready -->|否且有 default| Default["执行 default"]
    Ready -->|否且需等待| Enqueue["登记各 channel 等待者<br/>停放当前 G"]
    Enqueue --> Wake["某个通信成功唤醒"]
    Wake --> Clean["移除其他等待记录"]
    Clean --> Execute
放大图示

原始尺寸下可横向或纵向滚动;按 Escape 关闭。

正在绘制图示…

查看图源代码
flowchart TD
    Eval["求值 channel 与发送值"] --> Poll["检查各通信是否就绪<br/>一般路径使用随机化检查顺序"]
    Poll --> Ready{"存在可执行通信?"}
    Ready -->|是| Execute["完成一个通信<br/>执行对应 case"]
    Ready -->|否且有 default| Default["执行 default"]
    Ready -->|否且需等待| Enqueue["登记各 channel 等待者<br/>停放当前 G"]
    Enqueue --> Wake["某个通信成功唤醒"]
    Wake --> Clean["移除其他等待记录"]
    Clean --> Execute

这是一般流程。编译器会把零个、一个通信等简单形态改写成更简单的操作,不能说每个 select 都付出了同一套排序和多队列成本。case 数量、争用与是否真正阻塞,都会影响实际开销。[8:1]

timer:先区分“已经到期”和“接收方已经运行” ​

timer 保存一个未来触发条件。到期后,运行时安排通知或回调;接收这个通知的 G 还要被调度。time.NewTimer(d) 的约定是至少经过 d 后发送,不是实时系统的准点执行保证。[3:2]

入口得到什么适合什么情形清理或并发边界
time.NewTimer可 Stop/Reset 的一次性 Timer需要控制等待生命周期Stop 不关闭 C
time.After一次性时间 channel简单的一次等待没有返回 Timer 供手动 Stop/Reset
time.AfterFunc到期启动函数的 Timer回调形式C 为 nil;Stop/Reset 不代替回调完成同步
time.NewTicker重复通知的 Ticker周期触发慢接收者可能丢 tick;Stop 不关闭 channel

当前 GC 可以回收失去引用的 channel timer/ticker,不必为了“让 GC 能回收”而强制 Stop。不过“能回收”不等于业务不再需要管理生命周期;如果仍有人在等这个 channel,只 Stop 而不提供退出信号,就可能留下等待者。AfterFunc 的回调生命周期要按它自己的文档处理,不能套 channel timer 的结论。[3:3][9]

Stop、Reset:旧文章最容易过期的一处 ​

go
t := time.NewTimer(time.Hour)
fmt.Println("timer len/cap:", len(t.C), cap(t.C))
fmt.Println("stop:", t.Stop())
fmt.Println("reset stopped:", t.Reset(0))
<-t.C
fmt.Println("timer fired")

Go 1.27.2 的本次输出:

text
timer len/cap: 0 0
stop: true
reset stopped: false
timer fired

Reset 返回 false 表示此前已停止或已过期,不表示本次重置失败。程序把已停止的 timer 重新设为立即到期,随后正常收到通知。[3:4]

工具链时代channel timer 的重要差异
Go 1.23 之前旧的容量 1 异步通知可能留下过期时间值;Stop/Reset 的正确使用需要考虑排空与接收者协调
Go 1.23–1.26新实现提供同步 channel 语义;兼容选择与模块版本、asynctimerchan 等有关
本文 Go 1.27.2旧开关已移除,应用看到的 timer channel 始终为同步语义;不能继续用旧值恢复旧行为

Go 1.27.2 中,channel timer 的 Stop 返回后不会再收到来自停止前那次设置的陈旧时间值;Reset 返回后的接收不会得到旧设置产生的值。不要从旧文章机械复制“Stop 返回 false 就无条件 <-t.C”的代码:若没有待收值,那次读取自己就会阻塞。[3:5][10][11]

为了验证“已过旧 deadline,却尚未接收”的情况,测试用 testing/synctest 的虚拟时间先前进两秒,再重置一个原本一秒到期的 timer:

go
timer := time.NewTimer(time.Second)
time.Sleep(2 * time.Second)
if !timer.Reset(time.Second) {
    t.Fatal("unreceived timer should still be running")
}
select {
case <-timer.C:
    t.Fatal("stale value after Reset")
default:
}
time.Sleep(time.Second)
<-timer.C

完整测试放在 synctest 隔离环境中,本次结果:

text
=== RUN   TestResetRemovesStaleValue
--- PASS: TestResetRemovesStaleValue (0.00s)

这验证当前 API 的旧值消除语义,没有用真实 Sleep(2s) 去赌一个窄竞态,也没有证明旧工具链行为。[3:6][12]

AfterFunc 要单独看:Stop 返回 false 时,回调可能已经开始,它不会等回调结束;Reset 也可能安排下一次回调与前一次重叠。需要等回调收尾,就用额外的完成信号或其他同步。这与上面 channel timer 没有陈旧值,是不同的保证。[3:7]

Go 1.27.2 如何处理被移除的开关 ​

文末还有一个 go 1.22 的独立小模块,在当前工具链运行仍输出:

text
old module timer capacity: 0

即使这个旧模块传 GODEBUG=asynctimerchan=0,结果也相同。尝试把环境变量设为旧值 1,本次在 main 运行前失败:

text
fatal error: removed GODEBUG "asynctimerchan" set to old value "1" in environment (https://go.dev/doc/godebug#go-127)

退出码为 2。另一个独立 fixture 在 go.mod 中写 godebug asynctimerchan=1,构建也失败:

text
go: error loading go.mod:
go.mod:5: removed GODEBUG "asynctimerchan" set to old value "1" (https://go.dev/doc/godebug#go-127)

退出码为 1。新工具链接受被移除设置的最终默认值,但不会让旧值悄悄恢复旧实现;环境中的旧值还会导致启动失败。迁移时应检查部署环境和模块配置,不能只改业务代码。[11:1][13]

还有一个看源码容易产生的疑问:NewTimer 里仍可看到 make(chan Time, 1)。这不推翻同步语义,因为运行时对 timer channel 做了特殊处理,chanlen/chancap 对应用呈现 0,并配合停止、重置撤销不可见的旧发送。应用依赖的是 API 语义,内部暂存方案不是一个可绕过约定的容量 1 通道。[14]

timer 怎样与调度、网络超时接在一起 ​

当前运行时为 P 管理 timer 堆,按到期时间安排处理;Go 1.27.2 的具体结构是四叉堆,不应照旧资料一概写成“全局二叉小根堆”。任务调度会检查到期 timer,也会结合下一次到期时间决定可以等待多久;新出现更早的 timer 还需要唤醒相应的等待。[15][16]

底层等待使用运行时的单调时间。应用测耗时也应使用 start := time.Now() 配合 time.Since(start):Time 同时保存墙上时间和单调读数时,相减优先使用单调读数,避免系统校时把一次耗时改成负数。解析、序列化或某些时间操作会失去单调部分,不能把从文本恢复的 Time 当成仍带有原读数。[17]

这些设计让应用不用为每个 timer 创建一个专门休眠线程,但 timer 的创建、修改和唤醒仍有成本。高频循环反复创建 timer 不再必然构成旧版那种回收问题,却仍然可能有分配与调度开销;是否复用 Timer,应由实际热点与生命周期复杂度决定,本文没有为它编造性能倍率。

netpoller:把 I/O 等待交给事件,再把 G 唤醒 ​

对于当前 Linux 的可轮询 TCP 连接,Go 使用非阻塞描述符。读取暂时无法完成时,底层返回类似 EAGAIN 的结果,Go 的 poll 层登记等待并停放当前 G;epoll 报告相关事件后,运行时把等待者变成可运行,随后继续尝试读取。Darwin 和部分 BSD 对应 kqueue 路径。[4:2][18][19]

正在绘制图示…

可切换适合窗口或原始尺寸
查看图源代码
sequenceDiagram
    participant G as 业务 goroutine
    participant P as Go poll 层
    participant K as 内核 I/O 与就绪通知
    participant T as deadline timer
    G->>P: Read
    P->>K: 尝试非阻塞读取
    K-->>P: 暂不可读,返回 EAGAIN
    P-->>G: 停放 G,记录读等待者
    alt 描述符就绪
        K->>P: 就绪事件
    else 读 deadline 到期
        T->>P: 标记超时并唤醒等待者
    end
    P-->>G: 重新可运行
    G->>P: 继续读或取得错误
放大图示

原始尺寸下可横向或纵向滚动;按 Escape 关闭。

正在绘制图示…

查看图源代码
sequenceDiagram
    participant G as 业务 goroutine
    participant P as Go poll 层
    participant K as 内核 I/O 与就绪通知
    participant T as deadline timer
    G->>P: Read
    P->>K: 尝试非阻塞读取
    K-->>P: 暂不可读,返回 EAGAIN
    P-->>G: 停放 G,记录读等待者
    alt 描述符就绪
        K->>P: 就绪事件
    else 读 deadline 到期
        T->>P: 标记超时并唤醒等待者
    end
    P-->>G: 重新可运行
    G->>P: 继续读或取得错误

图中的“重新可运行”与“已经跑到业务代码”之间仍隔着调度。netpoller 做的是就绪通知,不替协议拼包:一次 Read 仍可能只返回部分数据;如果需要读满一个已知长度,要按 io.Reader 约定或用 io.ReadFull。普通文件、设备和外部库不一定都采用这条路径,不能宣传成“Go 所有 I/O 都不阻塞线程”。[4:3][20]

deadline 正好把 timer 与 netpoller 连起来。运行时的 pollDesc 保留读写等待者和 deadline timer,并用状态和序号处理关闭、重新设置 deadline 等情况下的过期事件,避免一条旧通知被当成当前操作的状态。[21]

用真正的 TCP 连接观察 deadline ​

完整程序在 127.0.0.1 创建临时监听端口。服务端接受连接后暂时不发送内容;客户端设置读 deadline,再读取一个字节:

go
err = c.SetReadDeadline(time.Now().Add(20 * time.Millisecond))
if err != nil { return err }
var b [1]byte
_, err = c.Read(b[:])
fmt.Println("TCP deadline:", errors.Is(err, os.ErrDeadlineExceeded))

本次输出:

text
TCP deadline: true

程序没有把实际返回耗时断言为恰好 20ms,而是验证错误语义。连接、监听器和服务端 goroutine 都有明确的关闭和等待收尾。这个实验使用真正的回环 TCP,能确认本机网络 deadline 路径;它没有模拟丢包、远端故障或 macOS kqueue,也不能只靠输出反推内部发生了多少次系统调用。[22]

读 deadline 是连接状态,不只是给这一行 Read 附上一个参数;成功设置后会影响适用的后续 I/O,必要时需更新或清除。网络库提供的 SetDeadline 也不等于任意阻塞函数都能靠 context 自动打断,仍要看被调用 API 是否支持取消。[22:1]

把结束条件写在机制旁边 ​

代码里出现什么应该立刻检查什么
循环内的 defer资源会不会一直留到整个函数结束?
select 的发送表达式未被选中时,这个表达式仍执行是否合理?
select 接收一个可能关闭的 channel关闭后是退出,还是将分支设 nil?
Timer.Stop 或 Ticker.Stop等待者凭什么退出?不要等它自动 close channel
AfterFunc 的 Stop/Reset有没有另行同步回调完成?
网络 deadline设置错误是否处理,后续连接使用是否需要清除?
依赖“毫秒级保证”的代码这只是到期条件,还是确实有系统提供的执行时限?

这些问题都围绕同一件事:结束条件要由程序表达,不能让一个看似方便的运行时机制替你猜。

依据与验证边界 ​

本次实际执行普通构建、所有正文行为、-d=defer、三轮基准、测试、race、vet,以及两个已移除设置的失败反例和旧模块兼容检查。没有进行旧版 Go 二进制对照,没有执行 kqueue,也没有测试真实网络故障或 timer 的实时性。基准来自本次 Linux/amd64、AMD EPYC 9V74 环境,不是输入样板的 Mac 数字。

完整代码与复现 ​

主模块与两个小模块分别保存。两个小模块用于隔离兼容设置,不能把它们的 go.mod 合并进主模块。失败用例应独立执行并核对非零退出码。

go.mod ​

go.mod
module example.com/runtime/mechanisms

go 1.27

main.go ​

代码块 · 101 行 · 点击收起 / 展开
go
package main

import (
	"errors"
	"fmt"
	"net"
	"os"
	"time"
)

func deferred() (n int) {
	n = 1
	defer fmt.Println("saved argument:", n)
	defer func() { n++; fmt.Println("closure sees:", n) }()
	return 10
}
func rhs() int { fmt.Println("send expression evaluated"); return 1 }

//go:noinline
func direct(n int) int { return n + 1 }

//go:noinline
func oneDefer(n int) (r int) { defer func() { r++ }(); return n }

//go:noinline
func loopDefers(n int) (r int) {
	for range n {
		defer func() { r++ }()
	}
	return
}

func tcpDeadline() error {
	ln, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		return err
	}
	defer ln.Close()
	accepted := make(chan error, 1)
	release := make(chan struct{})
	done := make(chan struct{})
	go func() {
		defer close(done)
		c, e := ln.Accept()
		accepted <- e
		if e != nil {
			return
		}
		defer c.Close()
		<-release
	}()
	defer func() { close(release); ln.Close(); <-done }()
	c, err := net.DialTimeout("tcp", ln.Addr().String(), time.Second)
	if err != nil {
		return err
	}
	defer c.Close()
	if err = <-accepted; err != nil {
		return err
	}
	if err = c.SetReadDeadline(time.Now().Add(20 * time.Millisecond)); err != nil {
		return err
	}
	var b [1]byte
	_, err = c.Read(b[:])
	fmt.Println("TCP deadline:", errors.Is(err, os.ErrDeadlineExceeded))
	if !errors.Is(err, os.ErrDeadlineExceeded) {
		return fmt.Errorf("unexpected read result: %v", err)
	}
	return nil
}
func main() {
	fmt.Println("return:", deferred())
	fmt.Println("defer results:", direct(4), oneDefer(4), loopDefers(3))
	var disabled chan int
	select {
	case disabled <- rhs():
	default:
		fmt.Println("default selected")
	}
	c := make(chan int)
	close(c)
	for i := range 2 {
		select {
		case _, ok := <-c:
			fmt.Println("closed ready:", i, ok)
			c = nil
		default:
			fmt.Println("nil disabled:", i)
		}
	}
	t := time.NewTimer(time.Hour)
	fmt.Println("timer len/cap:", len(t.C), cap(t.C))
	fmt.Println("stop:", t.Stop())
	fmt.Println("reset stopped:", t.Reset(0))
	<-t.C
	fmt.Println("timer fired")
	if err := tcpDeadline(); err != nil {
		panic(err)
	}
}

main_test.go ​

代码块 · 51 行 · 点击收起 / 展开
go
package main

import (
	"testing"
	"testing/synctest"
	"time"
)

var result int

func TestDeferResults(t *testing.T) {
	if oneDefer(4) != 5 || loopDefers(3) != 3 {
		t.Fatal("wrong defer result")
	}
}
func TestResetRemovesStaleValue(t *testing.T) {
	synctest.Test(t, func(t *testing.T) {
		timer := time.NewTimer(time.Second)
		time.Sleep(2 * time.Second)
		if !timer.Reset(time.Second) {
			t.Fatal("unreceived timer should still be running")
		}
		select {
		case <-timer.C:
			t.Fatal("stale value after Reset")
		default:
		}
		time.Sleep(time.Second)
		<-timer.C
	})
}
func TestTCPDeadline(t *testing.T) {
	if err := tcpDeadline(); err != nil {
		t.Fatal(err)
	}
}
func BenchmarkDirect(b *testing.B) {
	for b.Loop() {
		result = direct(4)
	}
}
func BenchmarkOneDefer(b *testing.B) {
	for b.Loop() {
		result = oneDefer(4)
	}
}
func BenchmarkLoopOneDefer(b *testing.B) {
	for b.Loop() {
		result = loopDefers(1)
	}
}

compat/oldmodule/go.mod ​

go.mod
module example.com/oldtimer

go 1.22

compat/oldmodule/main.go ​

go
package main

import (
	"fmt"
	"time"
)

func main() {
	t := time.NewTimer(time.Hour)
	defer t.Stop()
	fmt.Println("old module timer capacity:", cap(t.C))
}

negative/removed/go.mod ​

go.mod
module example.com/removedtimer

go 1.27

godebug asynctimerchan=1

negative/removed/main.go ​

go
package main

func main() {}

执行命令 ​

bash
go version
go build -o mechanisms .
./mechanisms
go test -count=1 -v ./...
go test -count=1 -race -v ./...
go vet ./...
go build -gcflags='-d=defer' -o mechanisms .
go test -run '^$' -bench . -benchmem -count=3
(cd compat/oldmodule && go run .)
(cd compat/oldmodule && GODEBUG=asynctimerchan=0 go run .)
# 下面两项故意失败,分别核对退出码;不要使用 set -e 提前中断。
GODEBUG=asynctimerchan=1 ./mechanisms
echo "$?"
(cd negative/removed && go build .)
echo "$?"

  1. Go 规范:Defer statements。 ↩︎ ↩︎ ↩︎ ↩︎

  2. Go 规范:Select statements,求值、选择、nil 与 default 行为。 ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  3. Go 1.27.2 time/sleep.go,Timer.Stop、NewTimer、Reset、After、AfterFunc 的当前契约。 ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  4. Go 1.27.2 internal/poll/fd_unix.go,Read 与 EAGAIN 后的等待;fd_poll_runtime.go。 ↩︎ ↩︎ ↩︎ ↩︎

  5. Go 1.27.2 os.Exit。 ↩︎

  6. Go 1.27.2 runtime/panic.go,defer 记录、open-coded defer 和展开路径。 ↩︎ ↩︎

  7. Go 1.27.2 cmd/compile/internal/ssagen/ssa.go,open-coded 条件与 defer 生成。 ↩︎ ↩︎

  8. Go 1.27.2 runtime/select.go,selectgo 的检查顺序、锁顺序、入队与清理;编译器 walk/select.go,简单形态改写。 ↩︎ ↩︎

  9. Go 1.27.2 time/tick.go,Ticker 回收、慢接收与 Stop 不关闭通道。 ↩︎

  10. Go 1.23 发布说明:Timer changes,当时的新旧 timer 兼容行为。 ↩︎

  11. Go 1.27 发布说明,移除 asynctimerchan 及对已移除 GODEBUG 设置的处理。 ↩︎ ↩︎

  12. Go 1.27.2 testing/synctest,隔离环境与虚拟时间。 ↩︎

  13. Go 1.27.2 runtime/runtime1.go,环境中已移除设置的校验;runtime/proc.go,启动阶段报告;internal/godebugs/table.go。 ↩︎

  14. Go 1.27.2 runtime/chan.go,chanlen、chancap 的 timer 特殊处理;runtime/time.go,旧发送的同步与撤销。 ↩︎

  15. Go 1.27.2 runtime/time.go,per-P timer、timerHeapN=4、到期检查与唤醒。 ↩︎

  16. Go 1.27.2 runtime/proc.go,timer、调度与网络等待的结合。 ↩︎

  17. Go 1.27.2 time:Monotonic Clocks。 ↩︎

  18. Go 1.27.2 runtime/netpoll_epoll.go,Linux 平台实现。 ↩︎

  19. Go 1.27.2 runtime/netpoll_kqueue.go,kqueue 平台实现。 ↩︎

  20. Go 1.27.2 io.Reader。 ↩︎

  21. Go 1.27.2 runtime/netpoll.go,pollDesc、读写 deadline 与状态序号。 ↩︎

  22. Go 1.27.2 net.Conn,deadline 的适用范围与 os.ErrDeadlineExceeded。 ↩︎ ↩︎

保留各书来源与许可 · 示例运行条件以正文为准